#include "net/socket.hh"
#include "syserr.hh"
#include <fcntl.h>
#include <unistd.h>
#include <netinet/in.h>

namespace wb
{
namespace net
{
static constexpr int kUDP = SOCK_DGRAM;
static constexpr int kTCP = SOCK_STREAM;

socket& socket::operator=(socket&& other) noexcept
{
    if (std::addressof(other) != this)
    {
        fd_ = std::exchange(other.fd_, -1);
    }
    return *this;
}

socket::~socket()
{
}

int socket::proto(socket_option::proto proto)
{
    switch (proto)
    {
        case socket_option::proto::tcp:
            return kTCP;
        case socket_option::proto::udp:
            return kUDP;
    }
    THROW_SYSERR("wb::net::socket::proto - Unknown proto");
}

bool socket::set_iotype(socket_option::iotype t)
{
    if (!is_valid())
    {
        return false;
    }
    int oflag = ::fcntl(fd_, F_GETFL, 0);
    if (oflag == -1)
    {
        return false;
    }
    oflag = (t == socket_option::iotype::bio)
                ? oflag & ~O_NONBLOCK    // remove NONBLOCK flag
                : (oflag | O_NONBLOCK);  // add NONBLOCK flag
    return (::fcntl(fd_, F_SETFL, oflag) == 0);
}

void socket::close_socket()
{
    if (fd_ != -1)
    {
        ::close(fd_);
        fd_ = -1;
    }
}

socket create_socket_for_connect(const socket_option& opts)
{
    socket sock{::socket(static_cast<int>(opts.d), socket::proto(opts.p), 0)};
    if (sock.native_handle() < 0)
    {
        THROW_SYSERR("wb::net::create_socket_for_connect");
    }
    if (opts.b == socket_option::iotype::nbio)
    {
        if (!sock.set_iotype(socket_option::iotype::nbio))
        {
            THROW_SYSERR("wb::net::socket::set_iotype");
        }
    }
    return sock;
}

socket create_socket_for_listen(const socket_option& opts,
                                const ip_address& addr,
                                uint16_t port,
                                int32_t backlog)
{
    socket sock = create_socket_for_connect(opts);
    socklen_t optlen;
    auto ret = ::setsockopt(sock.native_handle(),
                            SOL_SOCKET,
                            SO_REUSEPORT | SO_REUSEADDR,
                            &optlen,
                            sizeof(optlen));
    if (ret < 0)
    {
        THROW_SYSERR("wb::net::create_socket_for_listen::setsockopt");
    }

    union
    {
        sockaddr sa;
        sockaddr_in6 sin6;
    } server{};
    server.sin6.sin6_family = static_cast<int>(opts.d);
    server.sin6.sin6_port = ::htons(port);
    memcpy(server.sin6.sin6_addr.__in6_u.__u6_addr8,
           addr.data().data(),
           addr.domain() == domain_t::IPv4 ? ip_address::ipv4_len
                                           : ip_address::ipv6_len);
    if (::bind(sock.native_handle(), &server.sa, sizeof(server.sa)) < 0)
    {
        THROW_SYSERR("wb::net::create_socket_for_listen::bind");
    }

    if (opts.p == socket_option::proto::tcp)
    {
        auto ret = ::listen(sock.native_handle(), backlog);
        if (ret < 0)
        {
            THROW_SYSERR("wb::net::create_socket_for_listen::listen");
        }
    }

    return sock;
}
}  // namespace net
}  // namespace wb